package com.media.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.content.base.exception.XueChengPlusException;
import com.content.base.model.PageParams;
import com.content.base.model.PageResult;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.media.dto.QueryMediaParamsDto;
import com.media.dto.UploadFileDto;
import com.media.response.RestResponse;
import com.media.service.MediaFilesService;
import com.media.mapper.MediaFilesMapper;
import com.media.po.MediaFiles;
import com.media.service.MediaProcessService;
import com.media.vo.MediaFileVo;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.*;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * <p>
 * 媒资信息 服务实现类
 * </p>
 *
 * @author ling
 */
@Slf4j
@Service
public class MediaFilesServiceImpl extends ServiceImpl<MediaFilesMapper, MediaFiles> implements MediaFilesService {
    @Autowired
    private MinioClient minioClient;

    @Autowired
    MediaFilesService proxy;
    //普通文件桶
    @Value("${minio.bucket.files}")
    private String bucketName1;

    @Value("${minio.bucket.videofiles}")
    private String bucketName2;

    @Autowired
    private MediaFilesMapper mediaFilesMapper;

    @Autowired
    private MediaProcessService mediaProcessService;

    /**
     * 普通文件上传
     *
     * @param dto
     * @param companyId
     * @param absolutePath
     */
    @Override //不在这里加事务，是因为具有网络传输，时间可长可短，时间长时会导致数据库链接不够用
    public MediaFileVo uploadFiles(UploadFileDto dto, Long companyId, String absolutePath,String objectName) throws IOException {
        //对媒资信息进行保存
        MediaFiles mediaFiles = proxy.addMediaFiles(companyId, bucketName1, absolutePath, dto,objectName);

        MediaFileVo mediaFileVo = null;
        //上传文件
        mediaFileVo = uploadFiles(dto, bucketName1, absolutePath, mediaFiles);

        log.info("上传文件成功！{}", mediaFileVo);

        return mediaFileVo;
    }

    /**
     * 上传文件
     *
     * @param dto
     * @param absolutePath
     * @param mediaFiles
     * @return
     */
    @NotNull
    private MediaFileVo uploadFiles(UploadFileDto dto, String bucketName, String absolutePath, MediaFiles mediaFiles) {
        try {

            //创建Minio上传参数
            UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                    .bucket(bucketName)
                    .object(mediaFiles.getFilePath()) //桶下的文件路径
                    .filename(absolutePath)      //上传文件的路径
                    .contentType(dto.getFileType())
                    .build();

            minioClient.uploadObject(uploadObjectArgs);
        } catch (Exception e) {
            XueChengPlusException.cast("上传文件到文件系统失败！");
        }
        MediaFileVo mediaFileVo = new MediaFileVo();
        BeanUtils.copyProperties(mediaFiles, mediaFileVo);
        return mediaFileVo;
    }

    /**
     * 保存媒资信息
     *
     * @param companyId
     * @param bucketName
     * @param absolutePath
     * @param dto
     * @return
     * @throws IOException
     */
    @Transactional
    public MediaFiles addMediaFiles(Long companyId, String bucketName, String absolutePath, UploadFileDto dto,String objectName) throws IOException {
        //absolutePath是临时文件的路径
        //MD5算法进行摘要
        FileInputStream fileInputStream = new FileInputStream(absolutePath);
        String md5_id = DigestUtils.md5Hex(fileInputStream);

        //查询数据库中是否存在该媒资信息
        MediaFiles one = lambdaQuery().eq(MediaFiles::getFileId, md5_id).one();

        MediaFiles mediaFiles = new MediaFiles();
        if (one == null) {
            //以当天的年月日作为文件夹
            LocalDate now = LocalDate.now();
            //原始文件名
            String fileName1 = dto.getFilename();
            //最终文件名
            String fileName2 = md5_id + dto.getFilename().substring(fileName1.indexOf("."));

            //最终在MinIO中的路径
            String finalFileName = now.getYear() + "/" + now.getMonthValue() + "/" + now.getDayOfMonth() + "/" + fileName2;

            if(StringUtils.isNotEmpty(objectName)){
                finalFileName = objectName;
            }

            UUID uuid = UUID.randomUUID();
            //copy基本信息
            BeanUtils.copyProperties(dto, mediaFiles);
            mediaFiles.setId(uuid.toString());
            mediaFiles.setCompanyId(companyId);
            //todo 机构名称
            mediaFiles.setCompanyName(md5_id);
            //todo 文件名称类型给值
            mediaFiles.setFileType("001001");
            mediaFiles.setTags("课程图片");  //课程图片
            mediaFiles.setBucket(bucketName);
            mediaFiles.setFilePath(finalFileName);
            mediaFiles.setFileId(md5_id);
            mediaFiles.setUrl("/" + mediaFiles.getBucket() + "/" + finalFileName);
            mediaFiles.setStatus("1");

            save(mediaFiles);
        } else {
            XueChengPlusException.cast("不允许上传与已存在内容一致文件！");
        }
        return mediaFiles;
    }

    /**
     * 上传大文件检查
     *
     * @param fileMd5
     */
    @Override
    public RestResponse<Boolean> checkFile(String fileMd5) {
        //查询文件信息
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        if (mediaFiles != null) {
            //桶
            String bucket = mediaFiles.getBucket();
            //存储目录
            String filePath = mediaFiles.getFilePath();
            //文件流
            InputStream stream = null;
            try {
                stream = minioClient.getObject(
                        GetObjectArgs.builder()
                                .bucket(bucket)
                                .object(filePath)
                                .build());

                if (stream != null) {
                    stream.close();   //记得关闭流，不然有时候会导致资源泄露
                    //文件已存在
                    return RestResponse.success(true);
                }
            } catch (Exception e) {

            }
        }
        //文件不存在
        return RestResponse.success(false);
    }

    /**
     * 检查文件上传分块
     *
     * @param fileMd5
     * @param chunk
     * @return
     */
    @Override
    public RestResponse<Boolean> checkChunk(String fileMd5, int chunk) {
        //得到分块文件目录
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);

        //得到分块文件的路径
        String chunkFilePath = chunkFileFolderPath + chunk;
        try {
            //文件流
            InputStream stream = minioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(bucketName2)
                            .object(chunkFilePath)
                            .build());

            if (stream != null) {
                //分块文件已存在
                stream.close();
                return RestResponse.success(true);
            }
        } catch (Exception e) {

        }

        //分块文件不存在
        return RestResponse.success(false);
    }

    //得到分块文件的目录
    private String getChunkFileFolderPath(String fileMd5) {
        return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
    }

    @Override
    public RestResponse<Boolean> uploadChunk(String file, String fileMd5, int chunk) {
        //获得上传文件目录
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);

        //获得上传文件路径
        String chunkFilePath = chunkFileFolderPath + chunk;

        String mimeType = getMimeType(null);

        boolean b = addMediaFilesToMinIO(file, mimeType, bucketName2, chunkFilePath);
        if (!b) {
            XueChengPlusException.cast("上传分块文件失败！");
        }

        log.debug("上传分块文件成功:{}", chunkFilePath);
        return RestResponse.success(true);
    }

    private String getMimeType(String extension) {
        if (extension == null)
            extension = "";
        //根据扩展名取出mimeType
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
        //通用mimeType，字节流
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        if (extensionMatch != null) {
            mimeType = extensionMatch.getMimeType();
        }
        return mimeType;
    }

    /**
     * @param localFilePath 文件地址
     * @param bucket        桶
     * @param objectName    对象名称
     * @return void
     * @description 将文件写入minIO
     * @author Mr.M
     * @date 2022/10/12 21:22
     */
    public boolean addMediaFilesToMinIO(String localFilePath, String mimeType, String bucket, String objectName) {
        try {
            UploadObjectArgs testbucket = UploadObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)        //上传文件路径
                    .filename(localFilePath)   //本地文件路径
                    .contentType(mimeType)
                    .build();
            minioClient.uploadObject(testbucket);
            log.debug("上传文件到minio成功,bucket:{},objectName:{}", bucket, objectName);
            System.out.println("上传成功");
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("上传文件到minio出错,bucket:{},objectName:{},错误原因:{}", bucket, objectName, e.getMessage(), e);
            XueChengPlusException.cast("上传文件到文件系统失败");
        }
        return false;
    }

    /**
     * 合并文件，要求前端分块文件至少为5M
     *
     * @param fileMd5
     * @param fileName
     * @param chunkTotal
     * @return
     */
    @Override
    public RestResponse<Boolean> mergechunks(String fileMd5, String fileName, int chunkTotal) {
        if (chunkTotal > 0) {
            String chunkPath = getChunkFileFolderPath(fileMd5);
            List<ComposeSource> sources = new ArrayList<>(chunkTotal);
            for (int i = 0; i < chunkTotal; i++) {
                String chunkFilePath = chunkPath + '/' + i;
                ComposeSource one = ComposeSource.builder().bucket(bucketName2).object(chunkFilePath).build();
                sources.add(one);
            }

            //合并文件路径
            String mergeFilePath = chunkPath + '/' + fileName;
            ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder()
                    .bucket(bucketName2)
                    .object(mergeFilePath)
                    .sources(sources)
                    .build();

            try {
                //合并分块文件
                ObjectWriteResponse objectWriteResponse = minioClient.composeObject(composeObjectArgs);

                //合并文件完整性校验
                boolean flag = checkMergeFile(fileMd5, mergeFilePath);
                if (!flag) {
                    return RestResponse.validfail(false, "合并文件失败");
                }
                log.info("合并文件成功");

                //获得其文件大小
                StatObjectResponse statObjectResponse = minioClient.statObject(StatObjectArgs.builder().bucket(bucketName2).object(mergeFilePath).build());

                long size = statObjectResponse.size();

                //媒资文件信息入库
                UploadFileDto uploadFileDto = new UploadFileDto();
                uploadFileDto.setFilename(fileName);
                uploadFileDto.setFileType("课程视频");
                uploadFileDto.setRemark("");
                uploadFileDto.setFileSize(size);
                //TODO 机构id
                Long companyId = 6L;
                proxy.addBigMediaFiles(companyId, bucketName2, mergeFilePath, uploadFileDto, fileMd5);


                //=====清除分块文件=====
                clearChunkFiles(chunkPath, chunkTotal);
                return RestResponse.success(true);
            } catch (Exception e) {
                return RestResponse.validfail(false, "合并文件失败");
            }

        }

        return RestResponse.success(true);
    }

    /**
     * 清除分块文件
     *
     * @param chunkFileFolderPath 分块文件路径
     * @param chunkTotal          分块文件总数
     */
    private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal) {

        try {
            List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i)
                    .limit(chunkTotal)
                    .map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
                    .collect(Collectors.toList());

            RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket("video").objects(deleteObjects).build();
            Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
            results.forEach(r -> {
                DeleteError deleteError = null;
                try {
                    deleteError = r.get();
                } catch (Exception e) {
                    e.printStackTrace();
                    log.error("清楚分块文件失败,objectname:{}", deleteError.objectName(), e);
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
            log.error("清楚分块文件失败,chunkFileFolderPath:{}", chunkFileFolderPath, e);
        }
    }

    /**
     * 保存大文件媒资信息
     */
    public void addBigMediaFiles(Long companyId, String bucketName2, String mergeFilePath, UploadFileDto uploadFileDto, String fileMd5) {
        MediaFiles mediaFiles = new MediaFiles();
        BeanUtils.copyProperties(uploadFileDto, mediaFiles);

        mediaFiles.setId(fileMd5);
        mediaFiles.setFileId(fileMd5);
        mediaFiles.setCompanyId(companyId);
        mediaFiles.setBucket(bucketName2);
        mediaFiles.setFilePath(mergeFilePath);
        mediaFiles.setUrl("/" + bucketName2 + "/" + mergeFilePath);

        boolean save = proxy.save(mediaFiles);

        if (!save) {
            log.error("保存文件信息出错,{}", mediaFiles);
            XueChengPlusException.cast("保存文件信息出错");
        }

        //添加到待处理任务表
        mediaProcessService.addTask(mediaFiles);
    }

    /**
     * 校验合并文件完整性
     */
    private boolean checkMergeFile(String fileMd5, String mergeFilePath) {
        //获得合并文件的io流
        try {
            InputStream inputStream = minioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(bucketName2)
                            .object(mergeFilePath)
                            .build());
            //计算合并文件的md5
            String mergeFileMd5 = DigestUtils.md5Hex(inputStream);
            //比较md5
            return fileMd5.equals(mergeFileMd5);
        } catch (Exception e) {
            log.error("合并文件时，检查文件完整性出错，文件路径:{}", fileMd5);
        }
        return false;
    }

    /**
     * 从Minio中下载文件
     *
     * @param bucket
     * @param objectName
     * @throws IOException
     */
    public File downLoadFile(String bucket, String objectName) throws IOException {
        File file = null;
        FileOutputStream fileOutputStream = null;
        try {
            GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .build();

            InputStream object = minioClient.getObject(getObjectArgs);
            //创建临时文件
            file = File.createTempFile("minio", ".merge");
            fileOutputStream = new FileOutputStream(file);
            IOUtils.copy(object, fileOutputStream);

            return file;
        } catch (Exception e) {

        } finally {
            if (fileOutputStream != null) {
                fileOutputStream.close();
            }
        }
        return null;
    }

    /**
     * 媒资文件查询
     *
     * @param pageParams
     * @param queryMediaParamsDto
     */
    @Override
    public PageResult<MediaFileVo> getAllMediaFiles(PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {
        LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(StringUtils.isNotEmpty(queryMediaParamsDto.getFilename()), MediaFiles::getFilename, queryMediaParamsDto.getFilename())
                .eq(StringUtils.isNotEmpty(queryMediaParamsDto.getFileType()), MediaFiles::getFileType, queryMediaParamsDto.getFileType());

        Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
        Page<MediaFiles> mediaFilesPage = baseMapper.selectPage(page, queryWrapper);
        Page<MediaFileVo> mediaFileVoPage = new Page<>();
        BeanUtils.copyProperties(mediaFilesPage, mediaFileVoPage);
        PageResult<MediaFileVo> pageResult = new PageResult<>(mediaFileVoPage.getRecords(), mediaFileVoPage.getTotal(), pageParams.getPageNo(), pageParams.getPageSize());
        return pageResult;
    }
}
